package cn.skyquant.quant4j.jforex.strategy.titans;

import cn.skyquant.quant4j.jforex.sdk.enums.LabelType;
import cn.skyquant.quant4j.jforex.sdk.enums.OpenType;
import cn.skyquant.quant4j.jforex.sdk.strategy.*;
import cn.skyquant.quant4j.jforex.sdk.strategy.BaseConfig;
import cn.skyquant.quant4j.sdk.enums.TradeDirection;
import cn.skyquant.quant4j.sdk.util.time.CalendarUtils;
import com.dukascopy.api.*;
import com.dukascopy.api.feed.IBarFeedListener;
import com.dukascopy.api.feed.IFeedDescriptor;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Library("quant4j-api.jar;quant4j-sdk.jar;quant4j-jforex-sdk.jar;freemarker-2.3.28.jar;httpclient-4.5.6.jar;httpcore-4.4.10.jar;fastjson-1.2.49.jar;commons-codec-1.11.jar")
public class Titans extends BaseStrategy implements IBarFeedListener {

    @Configurable("货币")
    public Instrument i1 = Instrument.EURUSD;
    @Configurable("大K线")
    public Period bigPeriod = Period.DAILY;
    @Configurable("布林轨:计算K柱个数(也用作ATR个数)")
    public int boll_nums = 20;
    @Configurable("布林轨:价格选择")
    public IIndicators.AppliedPrice appliedPrice = IIndicators.AppliedPrice.CLOSE;
    @Configurable("布林轨:上下边宽度")
    public double boll_width = 2;
    @Configurable("布林轨:均线类型")
    public IIndicators.MaType boll_ma_type = IIndicators.MaType.WMA;
    @Configurable("止损:单次亏损为账户的百分比")
    public double simple_sl = 0.01;
    @Configurable("移动止损选择")
    public SLType slType = SLType.diaodeng_jilun;
    @Configurable("布林带止损选择")
    public BollSLType bollSLType = BollSLType.s2;
    @Configurable("吊灯:高点下方X倍ATR")
    public double sl_diaodeng = 1;
    @Configurable("棘轮:每多一根大K柱,增加Y倍ATR")
    public double sl_jilun = 0.03;
    @Configurable("多少倍波动率开启移仓")
    public double move_atr = 0.7;
    @Configurable("往前找多少根柱体")
    public int k_num = 5;
    @Configurable("多少倍柱体有效")
    public double limit_atr = 0.3;
    @Configurable("计算仓位时，是否统计曾经的亏损值")
    public boolean is_add_sl = true;
    @Configurable("订单成交日志")
    public boolean log_fill = true;
    @Configurable("订单提交日志")
    public boolean log_submit = true;
    @Configurable("订单移仓日志")
    public boolean log_move = true;

    final Map<Instrument, InstrumentEntity> insMap = new HashMap<>();//交易品种实体

    final Map<Instrument, Boll> boll_ask_map = new HashMap<>();
    final Map<Instrument, Boll> boll_bid_map = new HashMap<>();
    final Map<Instrument, Double> atr_map = new HashMap<>();
    final Map<Instrument, IBar> bar_valid_map = new HashMap<>();
    final Map<Instrument, Boolean> can_run_map = new HashMap<>();

    @Override
    protected String getVersion() {
        return "003";
    }

    @Override
    protected String getName() {
        return "Titans" + eaId;
    }

    @Override
    protected String getEaName() {
        return "Titans";
    }

    @Override
    protected void onTickEx(Instrument instrument, ITick tick) {
    }

    @Override
    protected void onBarEx(Instrument instrument, Period period, IBar askBar, IBar bidBar) {
    }

    @Override
    protected void onAccountEx(IAccount account) {
    }

    @Override
    public void onStopEx() {
        super.onStopEx();
    }

    @Override
    protected void init(InitEntity initEntity) {
        TitansConfig titansConfig = new TitansConfig(getName(), 0, 1, 0.2, i1);
        initEntity.a.add(titansConfig);
        context.subscribeToBarsFeed(i1, period, OfferSide.BID, this);
        context.subscribeToBarsFeed(i1, bigPeriod, OfferSide.BID, this);
        initEntity.r = true;
        insMap.put(i1, new InstrumentEntity(i1));
        IChart chart = context.getChart(i1);
        if (chart != null) {
            IFeedDescriptor feedDescriptor = chart.getFeedDescriptor();
            if (feedDescriptor != null) {
                feedDescriptor.setPeriod(bigPeriod);
                feedDescriptor.setOfferSide(OfferSide.ASK);
                chart.setFeedDescriptor(feedDescriptor);
                chart.add(indicators.getIndicator("BBANDS"), new Object[]{boll_nums, boll_width, boll_width, boll_ma_type.ordinal()});
                chart.repaint();
            }
        }
    }

    @Override
    public void onBar(Instrument instrument, Period period, OfferSide offerSide, IBar bar) {
        if (period.equals(bigPeriod)) {
            boolean f = calc(instrument, bar);
            can_run_map.put(instrument, f);
        }

        try {
            process(instrument, bar, period);
        } catch (JFException e) {
            err("process error:%s", e.getMessage());
        }
    }

    private boolean calc(Instrument instrument, IBar bar) {
        try {
            long barstart = history.getBarStart(bigPeriod, context.getTime());
            double[] atrs = indicators.atr(i1, bigPeriod, OfferSide.BID, boll_nums, Filter.WEEKENDS, 1, barstart, 0);
            if (atrs == null || atrs.length == 0) {
                return false;
            }
            double atr = atrs[0];
            atr_map.put(instrument, atr);

            List<IBar> barList = history.getBars(instrument, bigPeriod, OfferSide.ASK, Filter.WEEKENDS, k_num + 1, barstart, 0);
            if (barList.size() < k_num + 1) {
                return false;
            }

            IBar bar_valid = null;//有效K柱
            for (int i = barList.size() - 1; i >= 0; i--) {
                IBar bar_i = barList.get(i);
                double bar_diff = bar_i.getHigh() - bar_i.getLow();
                double limit_atr_value = limit_atr * atr;
                if (bar_diff > limit_atr_value) {
                    bar_valid = barList.get(i);
                    break;
                }
            }
            if (bar_valid == null) {
                return false;
            }
            bar_valid_map.put(instrument, bar_valid);

            double[][] bolls_ask = indicators.bbands(i1, bigPeriod, OfferSide.ASK, appliedPrice, boll_nums, boll_width, boll_width, boll_ma_type, Filter.WEEKENDS, 1, bar.getTime(), 0);
            double[][] bolls_bid = indicators.bbands(i1, bigPeriod, OfferSide.BID, appliedPrice, boll_nums, boll_width, boll_width, boll_ma_type, Filter.WEEKENDS, 1, bar.getTime(), 0);
            if (bolls_ask == null || bolls_ask.length != 3 || bolls_bid == null || bolls_bid.length != 3) {
                return false;
            }
            Boll boll_ask = new Boll(bolls_ask[0][0], bolls_ask[1][0], bolls_ask[2][0]);
            Boll boll_bid = new Boll(bolls_bid[0][0], bolls_bid[1][0], bolls_bid[2][0]);
            boll_ask_map.put(instrument, boll_ask);
            boll_bid_map.put(instrument, boll_bid);
        } catch (JFException e) {
            err("calc error:%s", e.getMessage());
        }
        return true;
    }

    private void process(Instrument instrument, IBar bar, Period period) throws JFException {
        boolean canrun = can_run_map.get(instrument);
        if (!canrun) {
            return;
        }
        ITimeDomain timeDomain0;
        ITimeDomain timeDomain1;
        try {
            timeDomain0 = dataService.getOfflineTimeDomain(0);
            timeDomain1 = dataService.getOfflineTimeDomain(-1);
        } catch (JFException e) {
            if (log_error) warn("onBar.getOfflineTimeDomain error:%s", e);
            return;
        }
        if (timeDomain0 == null || timeDomain1 == null) {
            if (log_error) warn("onBar.getOffline,TimeDomain is null");
            return;
        }
        //停盘时间不干活
        if (timeDomain1.getEnd() > context.getTime() || context.getTime() > timeDomain0.getStart()) {
            return;
        }

        List<IOrder> orderList = engine.getOrders(instrument);
        for (BaseConfig config : configList) {
            if (!(config instanceof TitansConfig)) {
                continue;
            }
            TitansConfig titansConfig = (TitansConfig) config;
            if (titansConfig.instrument != instrument) {
                continue;
            }
            OrderExManager orderExManager = getOrderExManager(titansConfig, orderList);
            if (period.equals(bigPeriod)) {
                processByConfig(orderExManager, titansConfig, bar);
            }

            if (period.equals(this.bigPeriod)) {
                move(orderExManager, titansConfig, bar);
            }
        }
    }

    //移动止损价
    private void move(OrderExManager orderExManager, TitansConfig x, IBar bar) throws JFException {
        if (!orderExManager.buyList.isEmpty()) {
            switch (slType) {
                case boll:
                    moveSL_boll(x.instrument, orderExManager.buyList, TradeDirection.LONG, bar);
                    break;
                case diaodeng_jilun:
                    moveDiaodeng(x.instrument, orderExManager.buyList, TradeDirection.LONG, bar);
                    break;
            }
        }
        if (!orderExManager.sellList.isEmpty()) {
            switch (slType) {
                case boll:
                    moveSL_boll(x.instrument, orderExManager.sellList, TradeDirection.SHORT, bar);
                    break;
                case diaodeng_jilun:
                    moveDiaodeng(x.instrument, orderExManager.sellList, TradeDirection.SHORT, bar);
                    break;
            }
        }
    }

    //开仓
    private void processByConfig(OrderExManager orderExManager, TitansConfig x, IBar bar) throws JFException {
        if (orderExManager.buyList.isEmpty()) {
            readyOpenOrder(x, TradeDirection.LONG, orderExManager.sellList);
        }
        if (orderExManager.sellList.isEmpty()) {
            readyOpenOrder(x, TradeDirection.SHORT, orderExManager.buyList);
        }
    }

    //吊灯止损
    private void moveDiaodeng(Instrument instrument, List<OrderEx> orderList, TradeDirection td, IBar bar) throws JFException {
        double atr = atr_map.get(instrument);
        InstrumentEntity ie = insMap.get(instrument);
        long contextTime = context.getTime();
        for (OrderEx orderEx : orderList) {
            IOrder order = orderEx.order;
            long filltime = order.getFillTime();
            if (contextTime - filltime < bigPeriod.getInterval()) {
                out("order[%s],profitpip=%s", order, order.getProfitLossInPips());
                if (order.getProfitLossInPips() > 20) {
                    order.setStopLossPrice(order.getOpenPrice());
                }
            } else {
                long k_time = (context.getTime() - filltime) / bigPeriod.getInterval();
                if (order.getOrderCommand().isLong()) {
                    double new_sl_price = new BigDecimal(bar.getHigh() - atr * (sl_diaodeng + sl_jilun * k_time)).setScale(ie.scale + 1, RoundingMode.UP).doubleValue();
                    if (new_sl_price > order.getStopLossPrice()) {
                        order.setStopLossPrice(new_sl_price);
                    }
                }
                if (order.getOrderCommand().isShort()) {
                    double new_sl_price = new BigDecimal(bar.getLow() + atr * (sl_diaodeng + sl_jilun * k_time)).setScale(ie.scale + 1, RoundingMode.DOWN).doubleValue();
                    if (new_sl_price < order.getStopLossPrice()) {
                        order.setStopLossPrice(new_sl_price);
                    }
                }
            }

        }

    }

    //布林带中轨法移动止损价
    private void moveSL_boll(Instrument instrument, List<OrderEx> orderList, TradeDirection td, IBar bar) throws JFException {
        Boll boll_ask = null;
        Boll boll_bid = null;
        switch (bollSLType) {
            case s1:
                boll_ask = boll_ask_map.get(instrument);
                boll_bid = boll_ask_map.get(instrument);
                break;
            case s2:
                long barstart = history.getBarStart(bigPeriod, context.getTime());
                double[][] bolls_ask = indicators.bbands(i1, bigPeriod, OfferSide.ASK, appliedPrice, boll_nums, boll_width, boll_width, boll_ma_type, Filter.WEEKENDS, 1, barstart, 0);
                double[][] bolls_bid = indicators.bbands(i1, bigPeriod, OfferSide.BID, appliedPrice, boll_nums, boll_width, boll_width, boll_ma_type, Filter.WEEKENDS, 1, barstart, 0);
                if (bolls_ask == null || bolls_ask.length != 3 || bolls_bid == null || bolls_bid.length != 3) {
                    return;
                }
                boll_ask = new Boll(bolls_ask[0][0], bolls_ask[1][0], bolls_ask[2][0]);
                boll_bid = new Boll(bolls_bid[0][0], bolls_ask[1][0], bolls_ask[2][0]);
                break;
        }
        if (boll_ask == null || boll_bid == null) {
            return;
        }
        double atr = atr_map.get(instrument);
        InstrumentEntity ie = insMap.get(instrument);
        long contextTime = context.getTime();
        for (OrderEx orderEx : orderList) {
            IOrder order = orderEx.order;
            long filltime = order.getFillTime();
            out("timediiff=%d,%d,profiit=%s",(contextTime - filltime),bigPeriod.getInterval(),order.getProfitLossInPips());
            if (contextTime - filltime < bigPeriod.getInterval()) {
                order.setStopLossPrice(order.getOpenPrice());
            } else if ((td.isLong() && bar.getClose() - boll_bid.m > move_atr * atr)
                    || (td.isShort() && boll_ask.m - bar.getClose() > move_atr * atr)) {

                double newSL = order.getOrderCommand().isLong() ?
                        new BigDecimal(boll_ask.m).setScale(ie.scale + 1, RoundingMode.UP).doubleValue() :
                        new BigDecimal(boll_bid.m).setScale(ie.scale + 1, RoundingMode.HALF_UP).doubleValue();
                if ((order.getOrderCommand().isLong() && newSL > order.getStopLossPrice())
                        || (order.getOrderCommand().isShort() && newSL < order.getStopLossPrice())) {
                    orderEx.order.setStopLossPrice(newSL);
                }
            }
        }
    }

    //准备开多单
    private void readyOpenOrder(TitansConfig x, TradeDirection td, List<OrderEx> otherList) throws JFException {
        InstrumentEntity ie = insMap.get(x.instrument);
        long barnextstart = history.getNextBarStart(bigPeriod, context.getTime());
        double equity = account.getEquity();
        double sl_money = equity * simple_sl;//每次能亏这么多钱
        if (is_add_sl) {
            sl_money -= total_sl;
        }
        //计算昨天的布林轨

        IBar bar = bar_valid_map.get(x.instrument);
        Boll boll_ask = boll_ask_map.get(x.instrument);
        Boll boll_bid = boll_bid_map.get(x.instrument);
        if (td.isLong() && bar.getClose() > boll_bid.m) {
            return;
        }
        if (td.isShort() && bar.getClose() < boll_ask.m) {
            return;
        }
        if (td.isLong() && bar.getClose() < bar.getOpen() && boll_ask.m - bar.getClose() < (bar.getOpen() - bar.getClose()) / 2) {
            return;
        }
        if (td.isShort() && bar.getClose() > bar.getOpen() && bar.getClose() - boll_ask.m < (bar.getClose() - bar.getOpen()) / 2) {
            return;
        }
        double price = new BigDecimal(td.isLong() ? boll_bid.m : boll_ask.m).setScale(ie.scale + 1, td.isLong() ? RoundingMode.DOWN : RoundingMode.UP).doubleValue();
        for (OrderEx orderEx : otherList) {
            if (
                    (td.isLong() && price > orderEx.order.getOpenPrice())
                            || (td.isShort() && price < orderEx.order.getOpenPrice())
            ) {
                return;
            }
        }
        double sl = td.isLong() ? bar.getLow() : bar.getHigh();
        double sl_price = Math.abs(price - sl);
        double amount = sl_money / ie.minMoney / (sl_price * ie.pipMulti) * ie.minAmount;
        amount = new BigDecimal(amount).setScale(ie.amountScale, RoundingMode.HALF_UP).doubleValue();
        double tp = 0;
        IEngine.OrderCommand orderCommand = td.isLong() ? IEngine.OrderCommand.BUYSTOP : IEngine.OrderCommand.SELLSTOP;
        OrderEx orderEx = new OrderEx(x.name, LabelType.M, x.index, 0, x.instrument, td);
        String label = orderEx.toString();
        int first_equit = (int) account.getEquity();
        TitansOrderComment orderComment = new TitansOrderComment(0, 0, OpenType.P, first_equit, context.getTime(), 0);
        String comment = orderComment.toString();
        if (engine.getOrder(orderEx.toString()) != null) {
            return;
        }
        long gtc = barnextstart - Period.FIVE_MINS.getInterval();
        engine.submitOrder(label, x.instrument, orderCommand, amount, price, 5, sl, tp, gtc, comment);
    }

    @Override
    protected void onMessageEx(IMessage message) {
        if (message.getOrder() == null) {
            return;
        }
        IOrder order = message.getOrder();
        OrderEx orderEx = new OrderEx(order, new TitansOrderComment());
        if (!orderEx.valid(getName())) {
            return;
        }
        Instrument instrument = order.getInstrument();
        InstrumentEntity ie = insMap.get(order.getInstrument());
        double amount = order.getAmount();
        double sl = order.getStopLossPrice();
        double tp = order.getTakeProfitPrice();
        double openprice = order.getOpenPrice();
        IEngine.OrderCommand orderCommand = order.getOrderCommand();
        switch (message.getType()) {
            case ORDER_FILL_OK:
                if (log_fill)
                    notif("Titans Fill, %s %s, open_price:%s,amount=%s,tp=%s,sl=%s", instrument.name(), orderCommand.name(), openprice, amount, tp, sl);
                break;
            case ORDER_SUBMIT_OK:
                if (log_submit) {
                    out("Titans Submit, label=%s,open_price=%s,amount=%s,tp=%s,sl=%s,create_time=%s,gtc=%s",
                            order.getLabel(),
                            order.getOpenPrice(),
                            order.getAmount(),
                            order.getTakeProfitPrice(),
                            order.getStopLossPrice(),
                            CalendarUtils.formatGMTStandard(order.getCreationTime()),
                            CalendarUtils.formatGMTStandard(order.getGoodTillTime()));

                }
                break;
            case ORDER_CHANGED_OK:
                if (log_move && message.getReasons().contains(IMessage.Reason.ORDER_CHANGED_SL)) {
                    out("Titans Move SL, label=%s,amount=%s,open_price=%s,sl=%s",
                            order.getLabel(),
                            order.getAmount(),
                            order.getOpenPrice(),
                            order.getStopLossPrice());
                }
                break;
            case ORDER_CLOSE_OK:
                double closePrice = order.getClosePrice();
                double profit = order.getProfitLossInUSD();
                if (profit > 0) {
                    info("%s,Titans %s %s Close,opentime=%s,fill_time=%s, open_price:%s,amount=%s,tp=%s,sl=%s,closePrice=%s,profit=%s,profit.pip=%s",
                            account.getBalance(), instrument.name(), orderCommand.name(),
                            CalendarUtils.formatGMTStandard(order.getCreationTime()), CalendarUtils.formatGMTStandard(order.getFillTime()),
                            openprice, amount, tp, sl, closePrice, profit, order.getProfitLossInPips());
                } else if (profit < 0) {
                    err("%s,Titans %s %s Close, opentime=%s,fill_time=%s, open_price:%s,amount=%s,tp=%s,sl=%s,closePrice=%s,profit=%s,profit.pip=%s",
                            account.getBalance(), instrument.name(), orderCommand.name(),
                            CalendarUtils.formatGMTStandard(order.getCreationTime()), CalendarUtils.formatGMTStandard(order.getFillTime()),
                            openprice, amount, tp, sl, closePrice, profit, order.getProfitLossInPips());
                }
                if (profit != 0) {
                    total_sl += profit;
                    if (total_sl > 0) {
                        total_sl = 0;
                    }
                }
                break;
        }
    }

    double total_sl = 0;

    private OrderExManager getOrderExManager(TitansConfig baseConfig, List<IOrder> orderList) {

        final OrderExManager o = new OrderExManager();
        for (IOrder order : orderList) {
            TitansOrderComment orderComment = new TitansOrderComment();
            OrderEx orderEx = new OrderEx(order, orderComment);
            if (!orderEx.valid(baseConfig.name, baseConfig.index) || !orderEx.orderComment.valid) {
                continue;
            }
            if (order.getOrderCommand() == IEngine.OrderCommand.BUY && order.getState() == IOrder.State.FILLED) {
                o.buyList.add(orderEx);
                o.buy_pl_usd += order.getProfitLossInUSD();
                o.buy_pl_pip += order.getProfitLossInPips();
                o.buy_c += order.getCommissionInUSD();
                if (o.buy_first == null || orderEx.seriesIndex < o.buy_first.seriesIndex) {
                    o.buy_first = orderEx;
                }
                if (o.buy_last == null || orderEx.seriesIndex > o.buy_last.seriesIndex) {
                    o.buy_last = orderEx;
                }
            } else if (order.getOrderCommand() == IEngine.OrderCommand.SELL && order.getState() == IOrder.State.FILLED) {
                o.sellList.add(orderEx);
                o.sell_pl_usd += order.getProfitLossInUSD();
                o.sell_pl_pip += order.getProfitLossInPips();
                o.sell_c += order.getCommissionInUSD();
                if (o.sell_first == null || orderEx.seriesIndex < o.sell_first.seriesIndex) {
                    o.sell_first = orderEx;
                }
                if (o.sell_last == null || orderEx.seriesIndex > o.sell_last.seriesIndex) {
                    o.sell_last = orderEx;
                }
            } else if (order.getOrderCommand() == IEngine.OrderCommand.SELLLIMIT && order.getState() == IOrder.State.OPENED) {
                o.sellLimitList.add(orderEx);
            } else if (order.getOrderCommand() == IEngine.OrderCommand.BUYLIMIT && order.getState() == IOrder.State.OPENED) {
                o.buyLimitList.add(orderEx);
            } else if (order.getOrderCommand() == IEngine.OrderCommand.BUYSTOP && order.getState() == IOrder.State.OPENED) {
                o.buyStopList.add(orderEx);
            } else if (order.getOrderCommand() == IEngine.OrderCommand.SELLSTOP && order.getState() == IOrder.State.OPENED) {
                o.sellStopList.add(orderEx);
            }
        }
        return o;
    }

    final static class TitansConfig extends BaseConfig {
        final Instrument instrument;//货币

        TitansConfig(String name, int index, double m, double lossLimit, Instrument instrument) {
            super(name, index, m, lossLimit);
            this.instrument = instrument;
        }

        @Override
        public String toString() {
            return "Titans_" + index
                    + "_" + instrument.name()
                    + "_m_" + m;
        }

        @Override
        public boolean isValid() {
            return instrument != null && m > 0;
        }
    }

    final static class TitansOrderComment extends BaseOrderComment {
        public TitansOrderComment() {
        }

        TitansOrderComment(int series_id, int series_index, OpenType type, int first_equit, long first_time, int this_profitloss) {
            super(series_id, series_index, type, first_equit, first_time, this_profitloss);
        }

    }

    enum SLType {
        boll("布林带"),
        diaodeng_jilun("吊灯+棘轮");
        final String desc;

        SLType(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return desc;
        }
    }

    enum BollSLType {
        s1("取昨日固定中轨值"),
        s2("取今日浮动中轨值");
        final String desc;

        BollSLType(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return desc;
        }
    }
}
